/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package cbsnclientparte;

import java.net.*;
import java.io.*;
import java.util.*;

/**
 *
 * @author Craig Butler (5668670)
 * @author Stephen Norman (616000X)
 * @version 1.0 22/05/2008
 */
final class Protocol
{
    
    /* Private Variables */
        
    // Relay Variables
    private String _relayAddress;
    private int _relayPort;
    
    // Connection to Relay
    private Socket _socket;
    
    // Reader Thread
    private Thread _readerThread;
    
    // Timer
    private Timer _dataCheckTmr;
    
    // Reader
    private Reader _reader;
    
    /* Variables for GoBack-N */
    int _expectedSeqNum = 1;
    int _currentSeqNum = 0;
    
    // The data in the packet
    byte[] data;
    
    /* Constructors */
    
    /**
     * Constructor for our protocol class
     */
    Protocol()
    {
        // Set relay variables
        _relayAddress = "";
        _relayPort = 4245;
        
        _dataCheckTmr = new Timer();
    }
    
    /**
     * Sets the relay's IP address
     * 
     * @param relayAddress The relay's IP address
     */
    public void setRelayAddress(String relayAddress)
    {
        // Set the address to the one provided
        _relayAddress = relayAddress;
    }

    /**
     * Automated protocol used for showing GoBack-N.
     */
    public void commenceAutomatedActions()
    {
        // Try to establish a connection to the relay
        if (establishConnection() == false)
        {
            // We cannot continue as we cannot contact the relay
            System.out.println(Util.getTime() + "We could not contact the relay. Make sure it is running and then restart this application.");
            System.exit(1);
        }
        
        // We have an open connection to the relay and can proceed to the next phase
        // which is making sure we can communicate with the server and that it can communicate
        // with us.
        testReadAndWrite();
    }
    
    /**
     * Attempts to open a connection to the relay.
     * 
     * @return The success or failure of the connection.
     */
    private boolean establishConnection()
    {
        // Variable if the connection succeeds
        boolean _connectionSuccess;
        
        try
        {
            // Open a connection to the relay
            _socket = new Socket(_relayAddress, _relayPort);
            
            // Successful connection
            _connectionSuccess = true;
        }
        catch (Exception ex)
        {
            // Display the error to the user
            System.out.println(Util.getTime() + "Conection Error: " + ex.getMessage());
            
            // Connection failed
            _connectionSuccess = false;
        }
        
        // Return if we were successful or if we failed
        return _connectionSuccess;
    }
    
    /**
     * The second phase of our application, testing 
     * communication b/w the client and server.
     */
    private void testReadAndWrite()
    {
        try
        {   
            // Create a new thread to read on
            DataInputStream ds = new DataInputStream(_socket.getInputStream());
            _reader = new Reader(ds);
            
            // Create the new thread
            _readerThread = new Thread(_reader);
            
            // Start the thread
            _readerThread.start();
            
            // Set the writer's output stream
            Writer.writer = new DataOutputStream(_socket.getOutputStream());
            
            // Finally we should go into the waiting phase
            _dataCheckTmr.schedule(new elapsedTime (), 5, 5);
        }
        catch (Exception ex)
        {
            // We encounted a problem at phase 2
            System.out.println(Util.getTime() + "Phase 2 Exception: " + ex.getMessage());
            
            // Exit the application
            System.exit(1);
        }
    }
    
    /**
     * This is where we process the packet
     * that we have receieved
     * 
     * @param packet The incoming packet to process
     */
    private void processPacket(byte[] packet)
    {
        try       
        {
            /* Variables for incoming data */
            
            // Is the incoming data corrupt
            boolean isPacketCorrupt = false;
            
            // The sequence number of the packet
            int seqNum;
            
            // The MD5 hash of the packet's data
            byte[] hash;
            
            // The length of the data in the packet
            int dataLength;

            // We need to extract the above variables 
            DataInputStream ds = new DataInputStream(new ByteArrayInputStream(packet));

            seqNum = ds.readInt();
            hash = new byte[32];
            ds.read(hash, 0, hash.length);
            dataLength = ds.readInt();
            
            try
            {
                data = new byte[dataLength];
            }
            catch (Exception ex)
            {
                System.out.println(Util.getTime() + "Data Length Error: " + ex.getMessage());
            }
            
            
            ds.read(data, 0, dataLength);        

            // Calculate the hash of the packet (now we have the data)
            String calcHash = Util.hashPacket(data);
            
            // Leave a blank line
            System.out.println();

            // Some dashes
            System.out.println("---------------------------------------------------");

            // Print out what is been sent
            System.out.println(Util.getTime() + "Receiving packet (contents below)");

            // Leave a blank line
            System.out.println();

            System.out.println("Sequence Number : " + seqNum);
            System.out.println("MD5 Hash        : " + new String(hash));
            System.out.println("Calculated Hash : " + calcHash);
            
            // Is the packet corrupt?
            if (calcHash.equals(new String(hash)))
            {
                // The packet was not corrupt so tell the user
                System.out.println("Packet Corrupt  : NO");
            }
            else
            {
                // The packet was corrupt 
                System.out.println("Packet Corrupt  : YES");
                isPacketCorrupt = true;
            }

            // Display the data length (we can't show that data)
            System.out.println("Data Length     : " + dataLength + " bytes");
            
            // Leave a blank line
            System.out.println();
            
            if (isPacketCorrupt == true)
            {
                // If the packet was corrupt then it has been dropped
                System.out.println(Util.getTime() + "Packet " + seqNum + " has been dropped due to corruption." );
                
                // Leave a blank line
                System.out.println();
                
                // Transmit an ACK back to the server saying we want the next
                // _expectedSeqNum packet again
                Writer.writePacket(_currentSeqNum, "This is an ACK packet".getBytes());
                
                // Stop processing this function
                return;
            }
            
            // We have received a packet that is not corrupt
            // We now have to check that it's sequence number is the one
            // we are expecting
            if (seqNum != _expectedSeqNum)
            {
                // This isn't a packet we were expecting so it will be dropped
                System.out.println(Util.getTime() + "Packet " + seqNum + " has been dropped due to unexpected sequence number." );
                
                // Leave a blank line
                System.out.println();
                
                // Transmit an ACK back to the server saying we want the next
                // _expectedSeqNum packet
                Writer.writePacket(_currentSeqNum, "This is an ACK packet".getBytes());
                
                // Stop processing this function
                return;
            }
            
            // Transmit an ACK back to the server saying we correctly received
            // this packet
            Writer.writePacket(_expectedSeqNum, "This is an ACK packet".getBytes());
            
            // We have received a packet that is not corrupt
            // and is the expected sequence number
            _expectedSeqNum++;
            _currentSeqNum++;
            
            // Process the packet
            
        }
        catch (Exception ex)
        {
            System.out.println("Packet Read Error: " + ex.getMessage()); 
        }
    }
    
    class elapsedTime extends TimerTask
    {
        public void run()
        {
            checkForData();
        }
        
        private void checkForData()
        {
            try
            {
                // Check if there is any data available
                if (_reader.isDataAvailable() == true)
                {
                    //_dataCheckTmr.cancel();

                    // Get the data
                    processPacket(_reader.getData());
                }
            }
            catch (Exception ex)
            {
                System.out.println("Timer Error: " + ex.getMessage());
            }
        }
    }
}
